/*
 * Copyright (c) 2022-2023 Shenzhen Kaihong Digital Industry Development Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

const { NapiLog } = require('./NapiLog');
const { Lexer, TokenType, code } = require('./lexer');
const { XMessage } = require('../message/XMessage');
const {
  AstObject,
  ConfigNode,
  ConfigTerm,
  ConfigArray,
  NodeRefType,
  ObjectType,
  Ast,
} = require('./ast');

function copy(obj) {
  return JSON.parse(JSON.stringify(obj));
}
class Parser {
  constructor() {
    this.lexer_ = new Lexer();
    this.current_ = {};
    this.srcQueue_ = [];
    this.astList = {};
  }
  parse(fn) {
    if (this.srcQueue_.indexOf(fn) === -1) {
      this.srcQueue_.push(fn);
    }

    while (this.srcQueue_.length > 0) {
      let includeList = [];
      let oneAst = this.parseOne(this.srcQueue_[0], includeList);
      if (oneAst === null) {
        return false;
      }

      this.astList[this.srcQueue_[0]] = {
        ast: oneAst,
        include: includeList,
      };
      this.srcQueue_.shift();
      this.srcQueue_ = this.srcQueue_.concat(includeList);
    }
    return this.astList;
  }
  convertAbsPath(includeBy, includePath) {
    if (navigator.userAgent.toLowerCase().indexOf('win') > -1) {
      //windows
      if (includePath[1] !== ':') {
        let currentSrc = includeBy.substring(
          0,
          includeBy.lastIndexOf('\\') + 1
        );
        includePath = currentSrc + includePath.replace(/\//g, '\\');

        includePath = includePath.replace(/\\\.\\/g, '\\');
      }
    } else if (includePath[0] !== '/') {
      let currentSrc = includeBy.substring(0, includeBy.lastIndexOf('/') + 1);
      includePath = currentSrc + includePath;
    }
    return includePath;
  }
  processInclude(includeList) {
    do {
      if (
        !this.lexer_.lex(this.current_) ||
        this.current_.type !== TokenType.STRING
      ) {
        this.dealWithError(
          this.lexer_ + 'syntax error, expect include path after ’#include‘'
        );
        return false;
      }

      let includePath = this.current_.strval;
      if (includePath.length <= 0) {
        this.dealWithError(
          this.lexer_ + 'include invalid file:' + this.includePath
        );
        return false;
      }
      includePath = this.convertAbsPath(this.srcQueue_[0], includePath);

      includeList.push(includePath);

      if (!this.lexer_.lex(this.current_)) {
        return false;
      }

      if (this.current_.type !== TokenType.INCLUDE) {
        break;
      }
    } while (true);

    return true;
  }
  parseOne(src, includeList) {
    if (!this.lexer_.initialize(src)) {
      return null;
    }

    if (!this.lexer_.lex(this.current_)) {
      return null;
    }

    if (
      this.current_.type === TokenType.INCLUDE &&
      !this.processInclude(includeList)
    ) {
      return null;
    }

    let rootNode = null;
    if (this.current_.type === TokenType.ROOT) {
      let preToken = copy(this.current_);
      preToken.type = TokenType.LITERAL;
      preToken.strval = 'root';
      rootNode = this.parseNode(preToken);
      if (rootNode === null) {
        return null;
      }
    } else if (this.current_.type !== TokenType.EOF) {
      this.dealWithError('syntax error, expect root node of end of file');
      return null;
    }

    if (
      !this.lexer_.lex(this.current_) ||
      this.current_.type !== TokenType.EOF
    ) {
      this.dealWithError('syntax error, expect EOF');
      return null;
    }

    let oneAst = new Ast(rootNode);
    return oneAst;
  }

  parseNode(name, bracesStart) {
    if (!bracesStart) {
      if (!this.lexer_.lex(this.current_) || this.current_.type !== code('{')) {
        this.dealWithError("syntax error, node miss '{'");
        return null;
      }
    }

    let node = new ConfigNode(name, NodeRefType.NODE_NOREF, '');
    let child;
    while (this.lexer_.lex(this.current_) && this.current_.type !== code('}')) {
      switch (this.current_.type) {
        case TokenType.TEMPLATE:
          child = this.parseTemplate();
          break;
        case TokenType.LITERAL:
          child = this.parseNodeAndTerm();
          break;
        default:
          this.dealWithError(
            "syntax error, except '}' or TEMPLATE or LITERAL for node '" +
              name.strval +
              ' at lineNo=' +
              child.lineno_ +
              "'"
          );
          return null;
      }
      if (child === null) {
        return null;
      }

      node.addChild(child);
    }

    if (this.current_.type !== code('}')) {
      this.dealWithError(this.lexer_ + "syntax error, node miss '}'");
      return null;
    }
    return node;
  }

  parseNodeAndTerm() {
    let name = copy(this.current_);
    if (!this.lexer_.lex(this.current_)) {
      this.dealWithError(this.lexer_ + 'syntax error, broken term or node');
      return null;
    }

    switch (this.current_.type) {
      case code('='):
        return this.parseTerm(name);
      case code('{'):
        return this.parseNode(name, true);
      case code(':'):
        if (this.lexer_.lex(this.current_)) {
          return this.parseNodeWithRef(name);
        }
        this.dealWithError('syntax error, unknown node reference type');
        break;
      default:
        this.dealWithError("syntax error, except '=' or '{' or ':'");
        break;
    }

    return null;
  }
  parseArray() {
    let array = new ConfigArray(this.current_);
    let arrayType = 0;

    while (this.lexer_.lex(this.current_) && this.current_.type !== code(']')) {
      if (this.current_.type === TokenType.STRING) {
        array.addChild(new AstObject('', ObjectType.PARSEROP_STRING, this.current_.strval, this.current_));
      } else if (this.current_.type === TokenType.NUMBER) {
        array.addChild(new AstObject('', ObjectType.PARSEROP_UINT64,
            this.current_.numval, this.current_, this.current_.baseSystem));
      } else {
        this.dealWithError(this.lexer_ + 'syntax error, except STRING or NUMBER in array');
        return nullptr;
      }

      if (arrayType === 0) {
        arrayType = this.current_.type;
      } else if (arrayType !== this.current_.type) {
        this.dealWithError(this.lexer_ + 'syntax error, not allow mix type array');
        return null;
      }

      if (this.lexer_.lex(this.current_)) {
        if (this.current_.type === code(']')) {
          break;
        } else if (this.current_.type !== code(',')) {
          this.dealWithError(this.lexer_ + "syntax error, except ',' or ']'");
          return null;
        }
      } else { return null; }
    }
    if (this.current_.type !== code(']')) {
      this.dealWithError(this.lexer_ + "syntax error, miss ']' at end of array");
      return null;
    }
    return array;
  }
  parseTerm(name) {
    if (!this.lexer_.lex(this.current_)) {
      this.dealWithError('syntax error, miss value of config term');
      return null;
    }
    let term = new ConfigTerm(name, null);
    switch (this.current_.type) {
      case TokenType.BOOL:
        term.addChild(new AstObject('', ObjectType.PARSEROP_BOOL, this.current_.numval, this.current_));
        break;
      case TokenType.STRING:
        term.addChild(new AstObject('', ObjectType.PARSEROP_STRING, this.current_.strval, this.current_));
        break;
      case TokenType.NUMBER:
        term.addChild(new AstObject('', ObjectType.PARSEROP_UINT64, this.current_.numval, this.current_, this.current_.baseSystem));
        break;
      case code('['): {
        let list = this.parseArray();
        if (list === null) {
          return null;
        } else {
          term.addChild(list);
        }
        break;
      }
      case code('&'):
        if (!this.lexer_.lex(this.current_) || (this.current_.type !== TokenType.LITERAL && this.current_.type !== TokenType.REF_PATH)) {
          this.dealWithError('syntax error, invalid config term definition');
          return null;
        }
        term.addChild(new AstObject('', ObjectType.PARSEROP_NODEREF, this.current_.strval, this.current_));
        break;
      case TokenType.DELETE:
        term.addChild(new AstObject('', ObjectType.PARSEROP_DELETE, this.current_.strval, this.current_));
        break;
      default:
        this.dealWithError('syntax error, invalid config term definition');
        return null;
    }

    if (!this.lexer_.lex(this.current_) || this.current_.type !== code(';')) {
      this.dealWithError("syntax error, miss ';'");
      return null;
    }

    return term;
  }
  parseTemplate() {
    if (
      !this.lexer_.lex(this.current_) ||
      this.current_.type !== TokenType.LITERAL
    ) {
      this.dealWithError('syntax error, template miss name');
      return null;
    }
    let name = copy(this.current_);
    let node = this.parseNode(name, false);
    if (node === null) {
      return node;
    }

    node.setNodeType(NodeRefType.NODE_TEMPLATE);
    return node;
  }

  parseNodeRef(name) {
    if (
      !this.lexer_.lex(this.current_) ||
      (this.current_.type !== TokenType.LITERAL &&
        this.current_.type !== TokenType.REF_PATH)
    ) {
      this.dealWithError(
        this.lexer_ + 'syntax error, miss node reference path'
      );
      return nullptr;
    }
    let refPath = this.current_.strval;
    let node = this.parseNode(name);
    if (node === null) {
      return null;
    }

    let configNode = node;
    configNode.setNodeType(NodeRefType.NODE_REF);
    configNode.setRefPath(refPath);
    return node;
  }
  parseNodeDelete(name) {
    let node = this.parseNode(name);
    if (node === null) {
      return null;
    }

    node.setNodeType(NodeRefType.NODE_DELETE);
    return node;
  }
  parseNodeCopy(name) {
    let nodePath = this.current_.strval;

    let node = this.parseNode(name);
    if (node === null) {
      return null;
    }

    let nodeCopy = node;
    nodeCopy.setNodeType(NodeRefType.NODE_COPY);
    nodeCopy.setRefPath(nodePath);

    return node;
  }
  parseNodeWithRef(name) {
    switch (this.current_.type) {
      case TokenType.REF_PATH:
      case TokenType.LITERAL:
        return this.parseNodeCopy(name);
      case code('&'):
        return this.parseNodeRef(name);
      case TokenType.DELETE:
        return this.parseNodeDelete(name);
      case code(':'):
        return this.parseNodeInherit(name);
      default:
        this.dealWithError('syntax error, unknown node type');
        break;
    }

    return null;
  }

  parseNodeInherit(name) {
    if (
      !this.lexer_.lex(this.current_) ||
      (this.current_.type !== TokenType.LITERAL &&
        this.current_.type !== TokenType.REF_PATH)
    ) {
      this.dealWithError('syntax error, miss node inherit path');
      return null;
    }

    let inheritPath = this.current_.strval;

    let node = this.parseNode(name);
    if (node === null) {
      return null;
    }

    node.setNodeType(NodeRefType.NODE_INHERIT);
    node.setRefPath(inheritPath);
    return node;
  }

  dealWithError(message) {
    let errorLocation =
      message +
      ' at source ' +
      this.current_.src +
      ' at line ' +
      this.current_.lineNo;
    XMessage.gi().send('error', errorLocation);
    NapiLog.logError(errorLocation + "'");
  }
}

module.exports = {
  Parser,
};
